Passa al contenuto principale

Sintassi per reti sincronizzate

Una rete sincronizzata si esprime come un module contenente registri, che sono espressi con reg il cui valore è inizializzato in risposta a reset_ ed aggiornato in risposta a fronti positivi del clock.

Gran parte della sintassi già vista per le reti combinatorie rimane valida anche qui, e dunque non la ripetiamo. Ci focalizziamo invece su come esprimere registri usando reg.

Istanziazione

Un registro si istanzia con statement simili a quelli per wire:

    reg [3:0] R1, R2;
reg R3, R4, R5;
Nomi in maiuscole e minuscolo

Verilog è case sensitive, cioè distingue come diversi nomi che differiscono solo per la capitalizzazione, come out e OUT.

Nel corso, utilizziamo questa feature per distinguere a colpo d'occhio reg e wire, utilizzando lettere maiuscole per i primi e minuscole per i secondi. Questo è particolarmente utile quando si hanno registri a sostegno di un wire, tipicamente un'uscita della rete o l'ingresso di un module interno.

Seguire questa convenzione non è obbligatorio, ma fortemente consigliato per evitare ambiguità ed errori che ne conseguono.

Collegamento a wire

Un reg si può utilizzare come "fonte di valore" per un wire. Questo equivale circuitalmente a collegare il wire all'uscita del reg.

    output out;
reg OUT;
assign out = OUT;

In questo caso, out seguirà sempre e in modo continuo il valore di OUT, propagandolo a ciò a cui viene collegato a sua volta. In questo caso non introduciamo nessun ritardo #T nell'assign perché si tratta di un semplice collegamento senza logica combinatoria aggiunta.

Allo stesso modo, si può collegare un reg all'ingresso di una rete.

    reg [3:0] X, Y;
add #( .N(4) ) a(
.x(X), .y(Y), .c_in(1'b0),
...
);
warning

Non ha invece alcun senso cercare di fare il contrario, ossia collegare direttamente un wire all'ingresso di un reg. Anche se questo ha senso circuitalmente, Verilog richiede di esprimere questo all'interno di un blocco always per indicare anche quando aggiornare il valore del reg.

Struttura generale di un blocco always

Il valore di un reg si aggiorna all'interno di blocchi always. La sintassi generale di questi blocchi è la seguente

always @( event ) [if( cond )] [ #T ] begin
[multiple statements]
end

Il funzionamento è il seguente: ogni volta che accade event, se cond è vero e dopo tempo T, vengono eseguiti gli statement indicati. Se lo statement è uno solo, si possono anche omettere begin e end.

Per Verilog, qui come statement si possono usare tutte le sintassi procedurali che si desiderano, incluse quelle discusse per le testbench che permettono di scrivere un classico programma "stile C". Per noi, no. Useremo questi blocchi in dei modi specifici per indicare

  1. come si comportano i registri al reset,
  2. come si comportano i registri al fronte positivo del clock.

Comportamento al reset

Per indicare il comportamento al reset useremo statement del tipo

always @(reset_ == 0) begin
R1 = 0;
end

Il funzionamento è facilmente intuibile: finché reset_ è a 0, il reg è impostato al valore indicato. Il blocco begin ... end può contenere l'inizializzazione di più registri. Tipicamente, raggrupperemo tutte le inizializzazioni in una descrizione, mentre le terremo separate in una sintesi.

Un registro può non essere inizializzato: in tal caso, il suo valore sarà non specificato, in Verilog X. Ricordiamo che questo significa che il registro ha un qualche valore misurabile, ma non è possibile determinare logicamente a priori e in modo univoco quale sarà.

In un blocco reset è indifferente l'uso di = o <= per gli assegnamenti (vedere sezione più avanti).

Valore assegnato al reset

Per la sintassi Verilog, a destra dell'assegnamento si potrebbe utilizzare qualunque espressione, sia questa costante (per esempio, il letterale 1'b0 o un parameter) o variabile (per esempio, il wire w).

Se pensiamo però all'equivalente circuitale, hanno senso solo valori costanti. Infatti, impostare un valore al reset equivale a collegare opportunamente i piedini preset_ e preclear_ del registro.

Aggiornamento al fronte positivo del clock

Per indicare il comportamento al fronte positivo del clock useremo statement del tipo

always @(posedge clock) if(reset_ == 1) #3 begin
OUT <= ~OUT;
end

Il funzionamento è il seguente: ad ogni fronte positivo del clock, se reset_ è a 1 e dopo 3 unità di tempo, il registro viene aggiornato con il valore indicato. Differentemente dal reset, qui si può utilizzare qualunque logica combinatoria per il calcolo del nuovo valore del registro.

L'unità di tempo (impostato a 3 in questo corso solo per convenzione, così come il periodo del clock a 10 unità) rappresenta il tempo di propagazione TpropagationT_{propagation} del registro, ossia il tempo che passa dal fronte del clock prima che il registro mostri in uscita il nuovo valore.

Tutti gli assegmenti in questi blocchi devono usare l'operatore <=, e non =. Come spiegato nella sezione più avanti, questo è necessario perché i registri simulati siano non-trasparenti.

Tipicamente usiamo registri multifunzionali, ossia che operano in maniera diversa in base allo stato della rete.

In una descrizione, questo si fa usando un singolo registro di stato STAR e indicando il comportamento dei vari registri multifunzionali al variare di STAR. Questo ci fa vedere in generale come si comporta l'intera rete al variare di STAR. In questa notazione, è lecito omettere un registro in un dato stato, implicando che quel registro conserva il valore precedentemente assegnato.

localparam S0 = 0, S1 = 1;
always @(posedge clock) if(reset_ == 1) #3 begin
casex(STAR)
S0: begin
A <= ~B;
B <= A;
STAR <= (A == 1'b0) ? S1 : S0;
end
S1: begin
A <= B;
B <= ~A;
STAR <= (B == 1'b1) ? S1 : S0;
end
endcase
end

In una sintesi, invece, si sintetizza ciascun registro individualmente come un multiplexer guidato da una serie di variabili di comando. Il multiplexer ha come ingressi tutti i risultati combinatori che il registro utilizza, e in base allo stato (da cui vengono generate le variabili di comando) solo uno di questi è utilizzato per aggiornare il registro al fronte positivo del clock. Questo è rappresentato in Verilog utilizzando le variabili di comando per discriminare il casex, e indicando un comportamento combinatorio per ciascun valore di queste variabili. In questa notazione, non è lecito omettere le operazioni di conservazione, mentre è lecito utilizzare non specificati per indicare comportamenti assegnati a più ingressi del multiplexer. Nell'esempio sotto, con 2'b1X si indica che a entrambi gli ingressi 10 e 11 del multiplexer è collegato il valore DAV_.

always @(posedge clock) if(reset_ == 1) #3 begin
casex({b1, b0})
2'b00: DAV_ <= 0;
2'b01: DAV_ <= 1;
2'b1X: DAV_ <= DAV_;
endcase
end

Limitazioni della simulazione: temporizzazione, non-trasparenza e operatori di assegnamento

Ci sono alcune differenze tra i registri, intesi come componenti elettronici, e i reg descritti in Verilog così come abbiamo visto. Queste differenze non sono d'interesse se non si fanno errori. In caso di errori, si potrebbero osservare comportamenti altrimenti inspiegabili, ed è per questo che è utile conoscere queste differenze per poter risalire alla fonte del problema.

I registri hanno caratteristiche di temporizzazione sia prima che dopo il fronte positivo del clock: ciascun ingresso va impostato almeno TsetupT_{setup} prima del fronte positivo, mantenuto fino ad almeno TholdT_{hold} dopo, e il valore in ingresso è rispecchiato in uscita solo dopo TpropagationT_{propagation}.

Date le semplici strutture sintattiche che utilizziamo, la simulazione non è così accurata e non considera TsetupT_{setup} e TholdT_{hold}. In particolare, il simulatore campiona i valori in ingresso non prima del fronte positivo, ma direttamente quando aggiorna il valore dei registri, ossia dopo TpropagationT_{propagation} dal fronte positivo del clock.

In altre parole: tutti i campionamenti e gli aggiornamenti dei registri sono fatti allo stesso tempo di simulazione, ossia TpropagationT_{propagation} dopo il fronte positivo del clock.

Questo porterebbe a violare la non-trasparenza dei registri, se non fosse per l'operatore di assegnamento <=, detto non-blocking assignement. Questo operatore si comporta in questo modo: tutti gli assegmenti <= contemporanei (ossia allo stesso tempo di simulazione) non hanno effetto l'uno sull'altro perché campionano il right hand side all'inizio del time-step e aggiornano il left hand side alla fine del time-step.

Questo simula correttamente la non-trasparenza dei registri, ma solo se tutti usano <=. Gli assegnamenti con =, detti blocking assignement, sono invece eseguiti completamente e nell'ordine in cui li incontra il simulatore (si assuma che quest'ordine sia del tutto casuale).

Al tempo di reset questo ci è indifferente, perché sono (circuitalmente) leciti solo assegnamenti con valori costanti e non si possono quindi creare anelli per cui è di interesse la non-trasparenza.